﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Indicates the key of a part.
    /// </summary>
    public class KeyIndicationElement : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyIndicationElement"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this key indication.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The element that directly owns this key indication.
        /// </para>
        /// </param>
        /// <param name="key">
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="Double"/>
        /// </para>
        /// <para>
        /// The key.
        /// It is an array of exactly 7 values (accidentals for notes C to B respectively).
        /// Each value must be in a the range [-1.0, 1.0].
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="key"/> has less or more than 7 elements.<br/>
        /// -or-<br/>
        /// Some of the values in the <paramref name="key"/> array is less than -1.0 or greater than 1.0.
        /// </exception>
        public KeyIndicationElement(CompositionPart packagePart, ModelElement owner, double[] key)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (key == null)
                throw new ArgumentNullException("key");
            if (key.Length != 7)
                throw new ArgumentException("The key array must have exactly 7 elements.", "key");
            if (key.Any(v => v < -1.0 || v > 1.0))
                throw new ArgumentException("All the values in the key array must be in the range [-1.0, 1.0].", "key");

            this.key = new double[7];
            key.CopyTo(this.key, 0);
            keyAttribute.Value = string.Format(
                "{0},{1},{2},{3},{4},{5},{6}",
                key[0].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[1].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[2].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[3].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[4].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[5].ToString(PackageVocabulary.PackageCulture.NumberFormat),
                key[6].ToString(PackageVocabulary.PackageCulture.NumberFormat)
                );
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyIndicationElement"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal KeyIndicationElement(XmlPackagePart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            keyAttribute = element.Attributes["key"];
            key = new double[7];

            var vals = keyAttribute.Value.Split(',');
            for (int i = 0; i < 7; ++i)
                key[i] = double.Parse(vals[i], PackageVocabulary.PackageCulture.NumberFormat);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "key_indication",
                PackageVocabulary.PackageNamespaceUri
                );

            keyAttribute = document.CreateAttribute("key");

            element.Attributes.Append(keyAttribute);

            return element;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Gets the key array.
        /// </summary>
        /// <returns>
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="double"/>
        /// </para>
        /// <para>
        /// An array of 7 values corresponding to the indicated key.
        /// </para>
        /// </returns>
        public double[] GetKey()
        {
            var keyCopy = new double[7];
            key.CopyTo(keyCopy, 0);

            return keyCopy;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the accidental associated with the specified note.
        /// </summary>
        /// <param name="note">
        /// <para>
        /// Type: <see cref="byte"/>
        /// </para>
        /// <para>
        /// The note for which the accidental is requested.
        /// It has to be a number from the range [1, 7] corresponding to notes C, D, E, ..., B respectively.
        /// </para>
        /// </param>
        /// <value>
        /// <para>
        /// Type: <see cref="double"/>
        /// </para>
        /// <para>
        /// The accidental associated with the specified note.
        /// </para>
        /// </value>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="note"/> is less than 1 or greater than 7.
        /// </exception>
        public double this[byte note]
        {
            get
            {
                if (note < 1 || note > 7)
                    throw new ArgumentOutOfRangeException("The note must be from the range [1, 7].", "note");

                return key[note - 1];
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the key value.
        /// </summary>
        private double[] key;
        /// <summary>
        /// Holds the XML attribute that holds the key value.
        /// </summary>
        private XmlAttribute keyAttribute;

        #endregion
    }

    /// <summary>
    /// Indicates the changemenet of the part's key.
    /// </summary>
    public class KeyChangementIndicationElement : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyChangementIndicationElement"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The part descriptor that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="measure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure at which this changement occurs.
        /// </para>
        /// </param>
        /// <param name="key">
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="Double"/>
        /// </para>
        /// <para>
        /// The key.
        /// It is an array of exactly 7 values (accidentals for notes C to B respectively).
        /// Each value must be in a the range [-1.0, 1.0].
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="key"/> has less or more than 7 elements.<br/>
        /// -or-<br/>
        /// Some of the values in the <paramref name="key"/> array is less than -1.0 or greater than 1.0.
        /// </exception>
        public KeyChangementIndicationElement(CompositionPart packagePart, PartDescriptorElement owner, MeasureElement measure, double[] key)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (measure == null)
                throw new ArgumentNullException("measure");
            if (key == null)
                throw new ArgumentNullException("key");

            keyIndication = new KeyIndicationElement(packagePart, this, key);
            keyIndicationElement.AppendChild(keyIndication.Element);

            this.measure = measure;
            measureAttribute.Value = measure.ID.ToString();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyChangementIndicationElement"/> class.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal KeyChangementIndicationElement(XmlPackagePart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            keyIndicationElement = element["key", PackageVocabulary.PackageNamespaceUri];
            keyIndication = new KeyIndicationElement(packagePart, this, keyIndicationElement.FirstChild as XmlElement);
            measureAttribute = element.Attributes["measure"];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "key_changement",
                PackageVocabulary.PackageNamespaceUri
                );

            measureAttribute = document.CreateAttribute("measure");

            keyIndicationElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "key",
                PackageVocabulary.PackageNamespaceUri
                );

            element.Attributes.Append(measureAttribute);
            element.AppendChild(keyIndicationElement);

            return element;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the key indication.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="KeyIndicationElement"/>
        /// </para>
        /// <para>
        /// The key indication.
        /// </para>
        /// </value>
        public KeyIndicationElement KeyIndication
        {
            get { return keyIndication; }
        }

        /// <summary>
        /// Gets the measure at which this changement occurs.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure at which this changement occurs 
        /// (i.e. the measure from which the new key is valid).
        /// </para>
        /// </value>
        public MeasureElement Measure
        {
            get
            {
                if (measure == null)
                    measure = PackagePart.Package.IDMap.FindObject<MeasureElement>(new Guid(measureAttribute.Value));

                return measure;
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="KeyIndication"/> property.
        /// </summary>
        private KeyIndicationElement keyIndication;
        /// <summary>
        /// Holds the XML element that holds the key indication.
        /// </summary>
        private XmlElement keyIndicationElement;
        /// <summary>
        /// Holds the value of the <see cref="Measure"/> property.
        /// </summary>
        private MeasureElement measure;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Measure"/> property.
        /// </summary>
        private XmlAttribute measureAttribute;

        #endregion
    }
}
